package com.hmy.finance.service.core.impl;

import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.hmy.ccp.error.exception.HMYRuntimeException;
import com.hmy.finance.api.bo.core.request.enums.FeeTypeEnum;
import com.hmy.finance.common.enums.BusinessBillTypeEnum;
import com.hmy.finance.common.enums.ProfitLossStatusEnum;
import com.hmy.finance.common.utils.FinanceCalculateUtil;
import com.hmy.finance.dal.po.DeviceDailyBillPo;
import com.hmy.finance.dal.po.DeviceProfitLossDetailPo;
import com.hmy.finance.dal.po.DeviceRentalRecordPo;
import com.hmy.finance.job.core.dto.dailyfee.DailyFeeBillGenerateProcessDto;
import com.hmy.finance.job.core.rdto.dailyfee.DailyFeeBillGenerateProcessRdto;
import com.hmy.finance.service.core.IDailyFeeGenerateService;
import com.hmy.finance.service.core.IDeviceDailyBillService;
import com.hmy.finance.service.core.IDeviceProfitLossDetailService;
import com.hmy.finance.service.core.IDeviceRentalRecordService;
import com.hmy.finance.service.core.dto.DeviceProfitLossDetailListQueryDto;
import com.hmy.finance.service.core.rdto.DeviceRentalRecordCalculateChargeDaysRdto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StopWatch;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

@Slf4j
@Service
public class DailyFeeGenerateServiceImpl implements IDailyFeeGenerateService {
    private final static String REDIS_LOCK_CONTRACT_PREFIX = "HMY:FINANCE:DL:CONTRACT:";
    private final static Long REDIS_LOCK_CONTRACT_TIMEOUT = 60L * 5;
    //    @Resource
//    private StringRedisTemplate redisTemplate;
    @Resource
    private DefaultIdentifierGenerator identifierGenerator;
    @Resource
    private IDeviceRentalRecordService deviceRentalRecordService;
    @Resource
    private IDeviceDailyBillService deviceDailyBillService;
    @Resource
    private IDeviceProfitLossDetailService deviceProfitLossDetailService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public DailyFeeBillGenerateProcessRdto generateDailyFeeBill(DailyFeeBillGenerateProcessDto processDto) {
        if (Objects.isNull(processDto) || Objects.isNull(processDto.getContractId())) {
            throw new HMYRuntimeException(Integer.valueOf("506010001"));
        }
//        RedisDistributedLock redisLock = new RedisDistributedLock(redisTemplate, REDIS_LOCK_CONTRACT_PREFIX + processDto.getContractId());
        try {
//            boolean locked = redisLock.tryLock(REDIS_LOCK_CONTRACT_TIMEOUT);
            //if can't get lock, throw exception
//            if (!locked) {
//                throw new HMYRuntimeException(Integer.valueOf("506010002"));
//            }

            //loop processDto#loopTimes, and generate daily fee bill
            List<DeviceRentalRecordPo> deviceRentalRecordPoList = new ArrayList<>();
            List<DeviceDailyBillPo> handleDeviceDailyBillList = new ArrayList<>();
            for (int i = 0; i < processDto.getLoopTimes(); i++) {
                processDto.setHandleDate(processDto.getHandleStartDate().plusDays(i));
                //get device rental record list by contractId and handleDate
                deviceRentalRecordPoList = deviceRentalRecordService.queryRentingDeviceRentalRecordByContractId(processDto.getContractId(), processDto.getHandleDate());
                //generate Device daily fee bill
                deviceRentalRecordPoList.forEach(deviceRentalRecord -> {
                    //if handle date before rentalStartTime, continue
                    if (processDto.getHandleDate().isBefore(deviceRentalRecord.getRentalStartTime().toLocalDate())) {
                        return;
                    }
                    DeviceDailyBillPo deviceDailyBill = generateDeviceDailyExpenseRecord(deviceRentalRecord, processDto.getHandleDate());
                    if (Objects.nonNull(deviceDailyBill)) {
                        handleDeviceDailyBillList.add(deviceDailyBill);
                    }
                });
                //handle contract daily expense record
            }
            //save device daily fee bill
            if (!handleDeviceDailyBillList.isEmpty()) {
                deviceDailyBillService.saveBatchDeviceDailyBill(handleDeviceDailyBillList);
            }

            return DailyFeeBillGenerateProcessRdto.success(processDto.getContractId(), processDto.getHandleDate());
        } finally {
//            redisLock.unlock();
        }
    }

    /**
     * Generate a device daily expense record based on the given device rental record and handle date.
     *
     * @param deviceRentalRecord the device rental record for which the daily expense record is generated
     * @param handleDate         the date on which the expense record is handled
     * @return the generated device daily expense record, or null if the record already exists for the handle date
     */
    private DeviceDailyBillPo generateDeviceDailyExpenseRecord(DeviceRentalRecordPo deviceRentalRecord, LocalDate handleDate) {
        DeviceDailyBillPo deviceDailyBill = deviceDailyBillService.queryDeviceDailyBillByDeviceRentalRecordIdWithDate(deviceRentalRecord.getId(), handleDate);
        //if deviceDailyBill is not null, means this day has been handled, return null
        if (Objects.nonNull(deviceDailyBill)) {
            log.info("Device daily bill is already exist.DeviceRentalRecordId:{},HandleDate:{}", deviceRentalRecord.getId(), handleDate);
            return null;
        }
        LocalDateTime feeIncurredTime = handleDate.atTime(LocalTime.MAX);
        deviceDailyBill = initializeDeviceDailyBill(deviceRentalRecord, feeIncurredTime);
        //generate device daily fee bill
        StopWatch deviceDailyBillStopWatch = new StopWatch();
        deviceDailyBillStopWatch.start();
        log.info("Begin to generate device daily fee bill.DeviceRentalRecordId:{},HandleDate:{}", deviceRentalRecord.getId(), handleDate);
        //Select All device profit loss detail List
        DeviceProfitLossDetailListQueryDto queryDto = new DeviceProfitLossDetailListQueryDto();
        queryDto.setDeviceRentalRecordId(deviceRentalRecord.getId());
        queryDto.setFeeIncurredTimeStart(handleDate.atStartOfDay());
        queryDto.setFeeIncurredTimeEnd(handleDate.atTime(LocalTime.MAX));
        queryDto.setFeeStatus(ProfitLossStatusEnum.WAIT_EFFECTIVE);
        List<DeviceProfitLossDetailPo> deviceProfitLossDetailList = deviceProfitLossDetailService.selectDeviceProfitLossDetailList(queryDto);
        //Calculate this day charge days
        DeviceRentalRecordCalculateChargeDaysRdto chargeDaysRdto = calculateChargeDays(deviceRentalRecord, handleDate);
        //filter deviceProfitLossDetailList,if it has pause profit loss,need change chargeDaysRdto
        if (deviceProfitLossDetailList != null && !deviceProfitLossDetailList.isEmpty()) {
            Optional<DeviceProfitLossDetailPo> pauseProfitLossOptional = deviceProfitLossDetailList.stream().filter(i -> i.getFeeType().equals(FeeTypeEnum.PAUSE_FEE)).findFirst();
            if (pauseProfitLossOptional.isPresent()) {
                chargeDaysRdto.setChargeDays(chargeDaysRdto.getChargeDays() - pauseProfitLossOptional.get().getDays());
                chargeDaysRdto.setFeeType(2);
                chargeDaysRdto.setPauseProfitLossDetailId(pauseProfitLossOptional.get().getId());
                chargeDaysRdto.setPauseDays(pauseProfitLossOptional.get().getDays());
            }
        } else {
            deviceProfitLossDetailList = new ArrayList<>();
        }
        //accumulation device rental record total rental days
        deviceRentalRecord.setTotalRentalDays(deviceRentalRecord.getTotalRentalDays() + chargeDaysRdto.getChargeDays());
        //calculate this day device rental unit price
        BigDecimal rentalUnitPrice = calculateRentalUnitPrice(deviceRentalRecord);
        deviceDailyBill.setFee(rentalUnitPrice.multiply(BigDecimal.valueOf(chargeDaysRdto.getChargeDays())));
        deviceDailyBill.setChargeDays(chargeDaysRdto.getChargeDays());
        deviceDailyBill.setType(chargeDaysRdto.getFeeType());
        //before calculate device rental record profit loss,need to initialize device rental record Rent difference on the first day of entry
        DeviceProfitLossDetailPo rentDifferenceDetail = initializeRentDifferenceDetail(deviceRentalRecord, handleDate, deviceDailyBill.getFee());
        //if rentDifferenceDetail is not null, means need to initialize rent difference detail, need to add to device profit loss detail list
        if (Objects.nonNull(rentDifferenceDetail)) {
            deviceProfitLossDetailService.save(rentDifferenceDetail);
            deviceProfitLossDetailList.add(rentDifferenceDetail);
        }
        return deviceDailyBill;
    }

    private DeviceProfitLossDetailPo initializeRentDifferenceDetail(DeviceRentalRecordPo deviceRentalRecord, LocalDate handleDate, BigDecimal thisDayFee) {
        //if device rental record rental start time % 1 != 0, means  the first day of rental is not full charge days, need to initialize rent difference detail
        double rentalStartDayChargeDays = FinanceCalculateUtil.getRentalStartDay(deviceRentalRecord.getRentalStartTime());
        if (rentalStartDayChargeDays % 1 != 0) {
            double diffDays = 1 - rentalStartDayChargeDays;
            BigDecimal diffAmount = deviceRentalRecord.getMonthlyRentalPrice().multiply(BigDecimal.valueOf(diffDays)).divide(BigDecimal.valueOf(30), 5, RoundingMode.HALF_UP);
            //if handle date equals rental start time, means this day is the first day of rental, need to initialize rent difference detail
            if (handleDate.equals(deviceRentalRecord.getRentalStartTime().toLocalDate())) {
                DeviceProfitLossDetailPo rentDifferenceDetail = new DeviceProfitLossDetailPo();
                rentDifferenceDetail.setDeviceRentalRecordId(deviceRentalRecord.getId());
                rentDifferenceDetail.setDeviceId(deviceRentalRecord.getDeviceId());
                rentDifferenceDetail.setDeviceNo(deviceRentalRecord.getDeviceNo());
                rentDifferenceDetail.setContractId(deviceRentalRecord.getContractId());
                rentDifferenceDetail.setContractNo(deviceRentalRecord.getContractNo());
                rentDifferenceDetail.setBusinessBillType(BusinessBillTypeEnum.ENTRY);
                rentDifferenceDetail.setFeeType(FeeTypeEnum.INITIAL_DAY_FEE_WAIVER);
                rentDifferenceDetail.setFee(diffAmount);
                rentDifferenceDetail.setFeeIncurredTime(handleDate.atTime(LocalTime.MAX));
                rentDifferenceDetail.setStatus(ProfitLossStatusEnum.WAIT_EFFECTIVE);
                rentDifferenceDetail.setRemarks("The first day of rental is not full charge days, need to initialize rent difference detail");
                return rentDifferenceDetail;
            } else
                // if handle date equals rental end time ,means this day is the last day of rental, need to check accumulate rental amount greater than monthly rental price, if not greater than, need to initialize rent difference detail
                if (Objects.nonNull(deviceRentalRecord.getRentalEndTime()) && handleDate.equals(deviceRentalRecord.getRentalEndTime().toLocalDate())) {
                    BigDecimal alreadyRentalAmount = deviceRentalRecord.getTotalRentalAmount().add(thisDayFee);
                    if (alreadyRentalAmount.compareTo(deviceRentalRecord.getMonthlyRentalPrice()) < 0 && deviceProfitLossDetailService.checkInitialDayFeeWaiver(deviceRentalRecord.getId())) {
                        DeviceProfitLossDetailPo rentDifferenceDetail = new DeviceProfitLossDetailPo();
                        rentDifferenceDetail.setDeviceRentalRecordId(deviceRentalRecord.getId());
                        rentDifferenceDetail.setDeviceId(deviceRentalRecord.getDeviceId());
                        rentDifferenceDetail.setDeviceNo(deviceRentalRecord.getDeviceNo());
                        rentDifferenceDetail.setContractId(deviceRentalRecord.getContractId());
                        rentDifferenceDetail.setContractNo(deviceRentalRecord.getContractNo());
                        rentDifferenceDetail.setBusinessBillType(BusinessBillTypeEnum.ENTRY);
                        rentDifferenceDetail.setFeeType(FeeTypeEnum.INITIAL_DAY_FEE);
                        rentDifferenceDetail.setFee(diffAmount);
                        rentDifferenceDetail.setFeeIncurredTime(handleDate.atTime(LocalTime.MAX));
                        rentDifferenceDetail.setStatus(ProfitLossStatusEnum.WAIT_EFFECTIVE);
                        rentDifferenceDetail.setRemarks("The first day of rental is not full charge days and Total rental amount less than monthly rental price, need to initialize rent difference detail");
                        return rentDifferenceDetail;
                    }
                }
        }
        return null;
    }

    /**
     * Initializes a DeviceDailyBillPo object using the provided DeviceRentalRecordPo and feeIncurredTime.
     *
     * @param deviceRentalRecord the DeviceRentalRecordPo object to initialize the DeviceDailyBillPo with
     * @param feeIncurredTime    the LocalDateTime when the fee was incurred
     * @return the initialized DeviceDailyBillPo object
     */
    private DeviceDailyBillPo initializeDeviceDailyBill(DeviceRentalRecordPo deviceRentalRecord, LocalDateTime feeIncurredTime) {
        DeviceDailyBillPo deviceDailyBill = new DeviceDailyBillPo();
        deviceDailyBill.setContractId(deviceRentalRecord.getContractId());
        deviceDailyBill.setCustomerId(deviceRentalRecord.getCustomerId());
        deviceDailyBill.setEntryFormId(deviceRentalRecord.getEntryFormId());
        deviceDailyBill.setDeviceRentalRecordId(deviceRentalRecord.getId());
        deviceDailyBill.setDeviceId(deviceRentalRecord.getDeviceId());
        deviceDailyBill.setDeviceNo(deviceRentalRecord.getDeviceNo());
        deviceDailyBill.setDailyFee(deviceRentalRecord.getDailyRentalPrice());
        deviceDailyBill.setMonthlyFee(deviceRentalRecord.getMonthlyRentalPrice());
        deviceDailyBill.setFeeStatus(Boolean.TRUE);
        deviceDailyBill.setFeeIncurredTime(feeIncurredTime);
        return deviceDailyBill;
    }

    /**
     * Calculate the rental unit price based on the device rental record.
     *
     * @param deviceRentalRecord the device rental record to calculate the unit price for
     * @return the calculated rental unit price
     */
    private BigDecimal calculateRentalUnitPrice(DeviceRentalRecordPo deviceRentalRecord) {
        //if the rental record already use monthly rental price,just return monthly rental price divide by 30
        if (deviceRentalRecord.getUseMonthlyRentalPrice() && deviceRentalRecord.getTotalRentalDays().compareTo(30d) >= 0) {
            return deviceRentalRecord.getMonthlyRentalPrice().divide(BigDecimal.valueOf(30), 5, RoundingMode.HALF_UP);
        }
        //if the rental record use daily rental price, need to check already charge days add this day charge days * daily rental price greater than monthly rental price
        //if grea/ter than, need to change to monthly rental price
        BigDecimal alreadyRentalAmount = deviceRentalRecord.getDailyRentalPrice().multiply(BigDecimal.valueOf(deviceRentalRecord.getTotalRentalDays()));
        BigDecimal deviceMonthlyAmountThreshold = calculateMonthlyRentalAmountThreshold(deviceRentalRecord);
        if (alreadyRentalAmount.compareTo(deviceMonthlyAmountThreshold) >= 0) {
            if (deviceMonthlyAmountThreshold.compareTo(deviceRentalRecord.getTotalRentalAmount()) > 0) {
                return deviceRentalRecord.getMonthlyRentalPrice().subtract(deviceRentalRecord.getTotalRentalAmount());
            } else {
                //trigger use monthly rental price
                deviceRentalRecord.setUseMonthlyRentalPrice(Boolean.TRUE);
                return BigDecimal.ZERO;
            }
        }
        // default set rental Unit Price as daily rental price
        return deviceRentalRecord.getDailyRentalPrice();
    }

    /**
     * Calculate the monthly rental amount threshold based on the device rental record.
     *
     * @param deviceRentalRecord the device rental record for calculation
     * @return the calculated monthly rental amount threshold
     */
    private BigDecimal calculateMonthlyRentalAmountThreshold(DeviceRentalRecordPo deviceRentalRecord) {
        double rentalStartDayChargeDays = FinanceCalculateUtil.getRentalStartDay(deviceRentalRecord.getRentalStartTime());
        if (deviceRentalRecord.getTotalRentalDays().compareTo(30d) >= 0 || rentalStartDayChargeDays % 1 == 0) {
            return deviceRentalRecord.getMonthlyRentalPrice();
        } else {
            return deviceRentalRecord.getMonthlyRentalPrice().multiply(BigDecimal.valueOf(29 + rentalStartDayChargeDays)).divide(BigDecimal.valueOf(30), 5, RoundingMode.HALF_UP);
        }
    }

    /**
     * Calculate the charge days for a device rental record based on the handle date.
     *
     * @param deviceRentalRecord the device rental record to calculate charge days for
     * @param handleDate         the date to handle the calculation for
     * @return the calculated charge days and fee type
     */
    private DeviceRentalRecordCalculateChargeDaysRdto calculateChargeDays(DeviceRentalRecordPo deviceRentalRecord, LocalDate handleDate) {
        DeviceRentalRecordCalculateChargeDaysRdto chargeDaysRdto = new DeviceRentalRecordCalculateChargeDaysRdto();
        //Check Device Rental Record is Termination
        if (Objects.nonNull(deviceRentalRecord.getRentalEndTime())) {
            //If handleDate equals rentalEndTime, means this day is the last day of rental, need use FinanceCalculateUtil to calculate termination day charge days
            if (handleDate.equals(deviceRentalRecord.getRentalEndTime().toLocalDate())) {
                log.info("HandleDate is equals rentalEndTime,This day termination charge.DeviceRentalRecordId:{},HandleDate:{},RentalEndTime:{}", deviceRentalRecord.getId(), handleDate, deviceRentalRecord.getRentalEndTime().toLocalDate());
                //Calculate termination day charge days
                double chargeDays = FinanceCalculateUtil.getRentalTerminationDay(deviceRentalRecord.getRentalEndTime());
                chargeDaysRdto.setChargeDays(chargeDays);
                chargeDaysRdto.setFeeType(9);
            } else if (handleDate.isAfter(deviceRentalRecord.getRentalEndTime().toLocalDate())) {
                log.info("HandleDate is after rentalEndTime,This day non-charge.DeviceRentalRecordId:{},HandleDate:{},RentalEndTime:{}", deviceRentalRecord.getId(), handleDate, deviceRentalRecord.getRentalEndTime().toLocalDate());
                //Non charge day
                chargeDaysRdto.setChargeDays(0d);
                chargeDaysRdto.setFeeType(10);
            } else {
                //Calculate normal day charge days
                log.info("HandleDate is before rentalEndTime,This day normal charge.DeviceRentalRecordId:{},HandleDate:{},RentalEndTime:{}", deviceRentalRecord.getId(), handleDate, deviceRentalRecord.getRentalEndTime().toLocalDate());
                double chargeDays = 1d;
                chargeDaysRdto.setChargeDays(chargeDays);
                chargeDaysRdto.setFeeType(0);
            }
        } else {
            //If handle Date equals rentalStartTime,means this day is the first day of rental, need use FinanceCalculateUtil to calculate start day charge days
            if (handleDate.equals(deviceRentalRecord.getRentalStartTime().toLocalDate())) {
                log.info("HandleDate is equals rentalStartTime,This day start charge.DeviceRentalRecordId:{},HandleDate:{},RentalStartTime:{}", deviceRentalRecord.getId(), handleDate, deviceRentalRecord.getRentalStartTime().toLocalDate());
                //Calculate start day charge days
                double chargeDays = FinanceCalculateUtil.getRentalStartDay(deviceRentalRecord.getRentalStartTime());
                chargeDaysRdto.setChargeDays(chargeDays);
                chargeDaysRdto.setFeeType(1);
            } else {
                //Calculate normal day charge days
                log.info("HandleDate is before rentalEndTime,This day normal charge.DeviceRentalRecordId:{},HandleDate:{},RentalEndTime:{}", deviceRentalRecord.getId(), handleDate, deviceRentalRecord.getRentalEndTime().toLocalDate());
                double chargeDays = 1d;
                chargeDaysRdto.setChargeDays(chargeDays);
                chargeDaysRdto.setFeeType(0);
            }
        }
        //When charge days greater than 0, need to check if this day is a pause day
        if (chargeDaysRdto.getChargeDays() > 0) {

        }
        return chargeDaysRdto;
    }


}
